readme_Obsidian_to_Facebook_012826

Obsidian에서 작성한 노트를 Python파일을 통해서 Facebook에 자동으로 옮기는 방법

1 단계

기본 페키지

pip install selenium 
pip install webdriver-manager 
pip install python-frontmatter 
pip install pillow # 선택적 (마크다운 처리) 
pip install markdown pip install beautifulsoup4

2 단계

# Obsidian parser
#!/usr/bin/env python3
"""
Obsidian 노트를 Facebook 게시물로 변환
"""

import frontmatter
import re
from pathlib import Path
from typing import Dict, List, Optional


class ObsidianToFacebook:
    """Obsidian 마크다운을 Facebook 형식으로 변환"""
    
    def __init__(self, vault_path: str):
        """
        Args:
            vault_path: Obsidian vault 경로
        """
        self.vault_path = Path(vault_path)
    
    def parse_note(self, note_path: str) -> Dict:
        """
        Obsidian 노트 파싱
        
        Args:
            note_path: 노트 파일 경로
            
        Returns:
            {
                'title': 제목,
                'content': 본문 (Facebook 형식),
                'images': 이미지 경로 리스트,
                'metadata': frontmatter 메타데이터,
                'tags': 태그 리스트
            }
        """
        note_path = Path(note_path)
        
        with open(note_path, 'r', encoding='utf-8') as f:
            post = frontmatter.load(f)
        
        metadata = post.metadata
        content = post.content
        
        # 이미지 추출
        images = self._extract_images(content, note_path.parent)
        
        # 마크다운을 Facebook 텍스트로 변환
        fb_content = self._markdown_to_facebook(content)
        
        # 태그 추출
        tags = self._extract_tags(content, metadata)
        
        # 제목 추출 (frontmatter 또는 첫 번째 헤딩)
        title = metadata.get('title', self._extract_title(content))
        
        return {
            'title': title,
            'content': fb_content,
            'images': images,
            'metadata': metadata,
            'tags': tags
        }
    
    def _extract_images(self, content: str, base_path: Path) -> List[str]:
        """
        마크다운에서 이미지 경로 추출
        
        Args:
            content: 마크다운 내용
            base_path: 노트의 기본 경로
            
        Returns:
            이미지 파일 전체 경로 리스트
        """
        images = []
        
        # Obsidian 형식: ![[image.png]]
        obsidian_pattern = r'!\[\[([^\]]+\.(jpg|jpeg|png|gif|webp))\]\]'
        obsidian_matches = re.findall(obsidian_pattern, content, re.IGNORECASE)
        
        # 표준 마크다운 형식: ![alt](image.png)
        markdown_pattern = r'!\[.*?\]\(([^\)]+\.(jpg|jpeg|png|gif|webp))\)'
        markdown_matches = re.findall(markdown_pattern, content, re.IGNORECASE)
        
        # Obsidian 형식 처리
        for match in obsidian_matches:
            image_name = match[0]
            image_path = self._find_image_in_vault(image_name)
            if image_path:
                images.append(str(image_path))
        
        # 표준 마크다운 형식 처리
        for match in markdown_matches:
            image_path_str = match[0]
            
            # 절대 경로인 경우
            if Path(image_path_str).is_absolute():
                if Path(image_path_str).exists():
                    images.append(image_path_str)
            else:
                # 상대 경로인 경우
                full_path = base_path / image_path_str
                if full_path.exists():
                    images.append(str(full_path.resolve()))
                else:
                    # vault에서 검색
                    found_path = self._find_image_in_vault(image_path_str)
                    if found_path:
                        images.append(str(found_path))
        
        return images
    
    def _find_image_in_vault(self, image_name: str) -> Optional[Path]:
        """
        Vault 전체에서 이미지 파일 검색
        
        Args:
            image_name: 이미지 파일명
            
        Returns:
            찾은 이미지의 전체 경로 또는 None
        """
        # Attachments 폴더 우선 검색
        attachments_dir = self.vault_path / "Attachments"
        if attachments_dir.exists():
            image_path = attachments_dir / image_name
            if image_path.exists():
                return image_path
        
        # 전체 vault 검색
        for image_path in self.vault_path.rglob(image_name):
            if image_path.is_file():
                return image_path
        
        return None
    
    def _markdown_to_facebook(self, content: str) -> str:
        """
        마크다운을 Facebook 텍스트로 변환
        
        Args:
            content: 마크다운 내용
            
        Returns:
            Facebook 형식 텍스트
        """
        # 이미지 링크 제거
        content = re.sub(r'!\[\[([^\]]+)\]\]', '', content)
        content = re.sub(r'!\[.*?\]\([^\)]+\)', '', content)
        
        # Obsidian 위키링크 변환: [[링크]] → 링크
        content = re.sub(r'\[\[([^\]|]+)(\|[^\]]+)?\]\]', r'\1', content)
        
        # 표준 마크다운 링크 변환: [텍스트](URL) → 텍스트 (URL)
        content = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', r'\1 (\2)', content)
        
        # 볼드 처리: **텍스트** → 텍스트 (Facebook은 자동 볼드 지원 안 함)
        content = re.sub(r'\*\*([^\*]+)\*\*', r'\1', content)
        content = re.sub(r'__([^_]+)__', r'\1', content)
        
        # 이탤릭 처리: *텍스트* → 텍스트
        content = re.sub(r'\*([^\*]+)\*', r'\1', content)
        content = re.sub(r'_([^_]+)_', r'\1', content)
        
        # 코드 블록 제거
        content = re.sub(r'```[\s\S]*?```', '', content)
        content = re.sub(r'`([^`]+)`', r'\1', content)
        
        # 헤딩 변환: # 제목 → 제목 (줄바꿈 추가)
        content = re.sub(r'^#{1,6}\s+(.+)

# 🤖 3단계: Facebook 자동 게시 (Selenium)

```python
#facebook selenium poster
#!/usr/bin/env python3
"""
Selenium을 사용한 Facebook 자동 게시
API 없이 브라우저 자동화로 게시
"""

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
import time
from pathlib import Path
from typing import List, Optional
import json


class FacebookPoster:
    """Selenium을 사용한 Facebook 자동 게시"""
    
    def __init__(self, headless: bool = False, profile_path: Optional[str] = None):
        """
        Args:
            headless: 브라우저 숨기기 (False = 브라우저 보임)
            profile_path: Chrome 프로필 경로 (로그인 유지용)
        """
        self.headless = headless
        self.profile_path = profile_path
        self.driver = None
    
    def start_browser(self):
        """브라우저 시작"""
        chrome_options = Options()
        
        # 프로필 경로 설정 (로그인 유지)
        if self.profile_path:
            chrome_options.add_argument(f"user-data-dir={self.profile_path}")
        
        # 헤드리스 모드
        if self.headless:
            chrome_options.add_argument("--headless=new")
        
        # 기타 옵션
        chrome_options.add_argument("--disable-blink-features=AutomationControlled")
        chrome_options.add_argument("--disable-dev-shm-usage")
        chrome_options.add_argument("--no-sandbox")
        chrome_options.add_argument("--window-size=1920,1080")
        
        # User Agent (봇 감지 방지)
        chrome_options.add_argument(
            "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/120.0.0.0 Safari/537.36"
        )
        
        # 드라이버 시작
        service = Service(ChromeDriverManager().install())
        self.driver = webdriver.Chrome(service=service, options=chrome_options)
        
        print("✅ 브라우저 시작됨")
    
    def login(self, email: str, password: str, save_session: bool = True):
        """
        Facebook 로그인
        
        Args:
            email: Facebook 이메일
            password: Facebook 비밀번호
            save_session: 세션 저장 여부
        """
        print("🔐 Facebook 로그인 중...")
        
        self.driver.get("https://www.facebook.com/")
        time.sleep(3)
        
        try:
            # 이메일 입력
            email_input = WebDriverWait(self.driver, 10).until(
                EC.presence_of_element_located((By.ID, "email"))
            )
            email_input.send_keys(email)
            
            # 비밀번호 입력
            password_input = self.driver.find_element(By.ID, "pass")
            password_input.send_keys(password)
            
            # 로그인 버튼 클릭
            login_button = self.driver.find_element(By.NAME, "login")
            login_button.click()
            
            print("⏳ 로그인 처리 중...")
            time.sleep(5)
            
            # 로그인 성공 확인
            if "login" not in self.driver.current_url.lower():
                print("✅ 로그인 성공!")
                return True
            else:
                print("❌ 로그인 실패")
                return False
                
        except Exception as e:
            print(f"❌ 로그인 오류: {e}")
            return False
    
    def is_logged_in(self) -> bool:
        """로그인 상태 확인"""
        try:
            self.driver.get("https://www.facebook.com/")
            time.sleep(3)
            
            # 로그인되어 있으면 뉴스피드가 보임
            return "login" not in self.driver.current_url.lower()
        except:
            return False
    
    def post_to_profile(self, text: str, images: List[str] = None):
        """
        개인 프로필에 게시
        
        Args:
            text: 게시물 텍스트
            images: 이미지 파일 경로 리스트
        """
        print("\n📝 게시물 작성 중...")
        
        try:
            # 개인 프로필로 이동
            self.driver.get("https://www.facebook.com/me")
            time.sleep(3)
            
            # "무슨 생각을 하고 계신가요?" 클릭
            # 여러 가능한 선택자 시도
            click_selectors = [
                "//span[contains(text(), '무슨 생각')]",
                "//span[contains(text(), 'What')]",
                "//div[@role='button' and contains(@aria-label, 'Create')]",
                "//div[contains(@class, 'x1i10hfl')]//span"
            ]
            
            create_post_clicked = False
            for selector in click_selectors:
                try:
                    create_button = WebDriverWait(self.driver, 10).until(
                        EC.element_to_be_clickable((By.XPATH, selector))
                    )
                    create_button.click()
                    create_post_clicked = True
                    print("✅ 게시물 작성 창 열림")
                    break
                except:
                    continue
            
            if not create_post_clicked:
                print("❌ 게시물 작성 버튼을 찾을 수 없습니다")
                return False
            
            time.sleep(2)
            
            # 텍스트 입력
            # contenteditable div 찾기
            text_selectors = [
                "//div[@contenteditable='true'][@role='textbox']",
                "//div[@contenteditable='true' and @aria-label]",
                "//div[contains(@class, 'notranslate')][@contenteditable='true']"
            ]
            
            text_entered = False
            for selector in text_selectors:
                try:
                    text_box = WebDriverWait(self.driver, 10).until(
                        EC.presence_of_element_located((By.XPATH, selector))
                    )
                    text_box.click()
                    time.sleep(1)
                    text_box.send_keys(text)
                    text_entered = True
                    print("✅ 텍스트 입력 완료")
                    break
                except:
                    continue
            
            if not text_entered:
                print("❌ 텍스트 입력 실패")
                return False
            
            time.sleep(2)
            
            # 이미지 업로드
            if images:
                print(f"📷 이미지 {len(images)}개 업로드 중...")
                
                # 사진/동영상 버튼 찾기
                photo_selectors = [
                    "//div[@aria-label='사진/동영상']",
                    "//div[@aria-label='Photo/video']",
                    "//input[@type='file' and @accept]"
                ]
                
                for selector in photo_selectors:
                    try:
                        if selector.startswith("//input"):
                            # 파일 입력 직접 찾기
                            file_input = self.driver.find_element(By.XPATH, selector)
                        else:
                            # 버튼 클릭 후 파일 입력 찾기
                            photo_button = WebDriverWait(self.driver, 10).until(
                                EC.element_to_be_clickable((By.XPATH, selector))
                            )
                            photo_button.click()
                            time.sleep(1)
                            
                            file_input = self.driver.find_element(By.XPATH, "//input[@type='file']")
                        
                        # 모든 이미지 경로를 한 번에 입력
                        all_images = "\n".join([str(Path(img).resolve()) for img in images])
                        file_input.send_keys(all_images)
                        
                        print(f"✅ 이미지 업로드 완료")
                        time.sleep(3)  # 업로드 대기
                        break
                    except Exception as e:
                        print(f"⚠️  이미지 업로드 시도 실패: {e}")
                        continue
            
            # 게시 버튼 클릭
            post_selectors = [
                "//div[@aria-label='게시'][@role='button']",
                "//div[@aria-label='Post'][@role='button']",
                "//span[text()='게시']",
                "//span[text()='Post']"
            ]
            
            for selector in post_selectors:
                try:
                    post_button = WebDriverWait(self.driver, 10).until(
                        EC.element_to_be_clickable((By.XPATH, selector))
                    )
                    post_button.click()
                    print("✅ 게시 버튼 클릭!")
                    break
                except:
                    continue
            
            # 게시 완료 대기
            time.sleep(5)
            
            print("🎉 게시물이 성공적으로 게시되었습니다!")
            return True
            
        except Exception as e:
            print(f"❌ 게시 오류: {e}")
            import traceback
            traceback.print_exc()
            return False
    
    def post_to_page(self, page_url: str, text: str, images: List[str] = None):
        """
        페이지에 게시
        
        Args:
            page_url: 페이지 URL (예: https://www.facebook.com/KAMCABQ/)
            text: 게시물 텍스트
            images: 이미지 파일 경로 리스트
        """
        print(f"\n📝 페이지 게시 중: {page_url}")
        
        try:
            # 페이지로 이동
            self.driver.get(page_url)
            time.sleep(3)
            
            # 이후 로직은 post_to_profile과 유사
            # (페이지 게시는 프로필 게시와 거의 동일)
            
            return self.post_to_profile(text, images)
            
        except Exception as e:
            print(f"❌ 페이지 게시 오류: {e}")
            return False
    
    def close(self):
        """브라우저 종료"""
        if self.driver:
            self.driver.quit()
            print("✅ 브라우저 종료됨")


# 사용 예제
if __name__ == "__main__":
    # 설정
    FACEBOOK_EMAIL = "your_email@example.com"
    FACEBOOK_PASSWORD = "your_password"
    
    # Chrome 프로필 경로 (선택사항 - 로그인 유지용)
    # Windows: C:/Users/YourName/AppData/Local/Google/Chrome/User Data
    # Mac: ~/Library/Application Support/Google/Chrome
    CHROME_PROFILE = None  # 또는 실제 경로
    
    # 브라우저 시작
    poster = FacebookPoster(headless=False, profile_path=CHROME_PROFILE)
    poster.start_browser()
    
    try:
        # 로그인 확인
        if not poster.is_logged_in():
            print("로그인 필요...")
            poster.login(FACEBOOK_EMAIL, FACEBOOK_PASSWORD)
        else:
            print("이미 로그인되어 있습니다!")
        
        # 게시물 작성
        text = """
📌 테스트 게시물

이것은 Obsidian에서 자동으로 게시된 테스트 메시지입니다.

#테스트 #자동화 #Python
        """
        
        # 이미지 (선택사항)
        images = [
            # "/path/to/image1.jpg",
            # "/path/to/image2.png"
        ]
        
        # 개인 프로필에 게시
        poster.post_to_profile(text, images)
        
        # 또는 페이지에 게시
        # poster.post_to_page("https://www.facebook.com/KAMCABQ/", text, images)
        
        print("\n✅ 완료!")
        
    except Exception as e:
        print(f"❌ 오류 발생: {e}")
        import traceback
        traceback.print_exc()
    
    finally:
        input("\n계속하려면 Enter를 누르세요...")
        poster.close()
```, r'\1\n', content, flags=re.MULTILINE)
        
        # 리스트 정리
        content = re.sub(r'^\s*[-*+]\s+', '• ', content, flags=re.MULTILINE)
        content = re.sub(r'^\s*\d+\.\s+', '', content, flags=re.MULTILINE)
        
        # 인용구 정리: > 텍스트 → 텍스트
        content = re.sub(r'^>\s+', '', content, flags=re.MULTILINE)
        
        # 수평선 제거
        content = re.sub(r'^[-*_]{3,}

# 🤖 3단계: Facebook 자동 게시 (Selenium)

{{CODE_BLOCK_3}}, '', content, flags=re.MULTILINE)
        
        # Obsidian 메타데이터 제거 (::)
        content = re.sub(r'^[^:]+::\s*.+

# 🤖 3단계: Facebook 자동 게시 (Selenium)

{{CODE_BLOCK_3}}, '', content, flags=re.MULTILINE)
        
        # 연속된 빈 줄을 하나로
        content = re.sub(r'\n{3,}', '\n\n', content)
        
        # 앞뒤 공백 제거
        content = content.strip()
        
        return content
    
    def _extract_tags(self, content: str, metadata: Dict) -> List[str]:
        """
        태그 추출
        
        Args:
            content: 마크다운 내용
            metadata: frontmatter 메타데이터
            
        Returns:
            태그 리스트 (# 포함)
        """
        tags = []
        
        # frontmatter에서 태그 추출
        if 'tags' in metadata:
            fm_tags = metadata['tags']
            if isinstance(fm_tags, list):
                tags.extend([f"#{tag.strip('#')}" for tag in fm_tags])
            elif isinstance(fm_tags, str):
                tags.append(f"#{fm_tags.strip('#')}")
        
        # 본문에서 해시태그 추출
        hashtag_pattern = r'#[가-힣a-zA-Z0-9_]+'
        content_tags = re.findall(hashtag_pattern, content)
        tags.extend(content_tags)
        
        # 중복 제거
        tags = list(set(tags))
        
        return tags
    
    def _extract_title(self, content: str) -> str:
        """
        본문에서 제목 추출 (첫 번째 # 헤딩)
        
        Args:
            content: 마크다운 내용
            
        Returns:
            제목 또는 빈 문자열
        """
        heading_match = re.search(r'^#\s+(.+)

# 🤖 3단계: Facebook 자동 게시 (Selenium)

{{CODE_BLOCK_3}}, content, re.MULTILINE)
        if heading_match:
            return heading_match.group(1).strip()
        return ''
    
    def format_for_facebook(self, parsed_data: Dict, 
                           include_title: bool = True,
                           include_tags: bool = True) -> str:
        """
        Facebook 게시물 최종 포맷팅
        
        Args:
            parsed_data: parse_note() 결과
            include_title: 제목 포함 여부
            include_tags: 태그 포함 여부
            
        Returns:
            Facebook 게시물 텍스트
        """
        parts = []
        
        # 제목 추가
        if include_title and parsed_data['title']:
            parts.append(f"📌 {parsed_data['title']}")
            parts.append("")  # 빈 줄
        
        # 본문 추가
        if parsed_data['content']:
            parts.append(parsed_data['content'])
            parts.append("")  # 빈 줄
        
        # 태그 추가
        if include_tags and parsed_data['tags']:
            tags_str = " ".join(parsed_data['tags'])
            parts.append(tags_str)
        
        return "\n".join(parts).strip()


# 사용 예제
if __name__ == "__main__":
    # 테스트
    vault_path = "/path/to/your/obsidian/vault"
    parser = ObsidianToFacebook(vault_path)
    
    note_path = vault_path + "/test-note.md"
    
    # 노트 파싱
    result = parser.parse_note(note_path)
    
    print("=" * 70)
    print("파싱 결과")
    print("=" * 70)
    print(f"\n제목: {result['title']}")
    print(f"\n이미지: {result['images']}")
    print(f"\n태그: {result['tags']}")
    print(f"\n내용:\n{result['content']}")
    
    # Facebook 형식으로 포맷팅
    fb_post = parser.format_for_facebook(result)
    print("\n" + "=" * 70)
    print("Facebook 게시물 미리보기")
    print("=" * 70)
    print(fb_post)

🤖 3단계: Facebook 자동 게시 (Selenium)

{{CODE_BLOCK_3}}